/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved. */ package org.pentaho.reporting.platform.plugin; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import javax.activation.DataHandler; import javax.mail.AuthenticationFailedException; import javax.mail.Authenticator; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.PasswordAuthentication; import javax.mail.SendFailedException; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import javax.mail.util.ByteArrayDataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.dom4j.Document; import org.dom4j.Node; import org.pentaho.platform.api.engine.IAcceptsRuntimeInputs; import org.pentaho.platform.api.repository.IContentItem; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.pentaho.platform.util.messages.LocaleHelper; import org.pentaho.reporting.platform.plugin.messages.Messages; /** * Creation-Date: 27.09.2009, 00:30:00 * * @author Pedro Alves - WebDetails */ public class SimpleEmailComponent implements IAcceptsRuntimeInputs { /** * The logging for logging messages from this component */ private static final Log log = LogFactory.getLog( SimpleEmailComponent.class ); private static final String MAILER = "smtpsend"; //$NON-NLS-1$ private Map<String, Object> inputs; private String outputType; // Inputs public static final String INPUT_TO = "to"; public static final String INPUT_FROM = "from"; public static final String INPUT_CC = "cc"; public static final String INPUT_BCC = "bcc"; public static final String INPUT_SUBJECT = "subject"; public static final String INPUT_MESSAGEPLAIN = "message-plain"; public static final String INPUT_MESSAGEHTML = "message-html"; public static final String INPUT_MIMEMESSAGE = "mime-message"; // beans private String to; private String from; private String cc; private String bcc; private String subject; private Object messagePlain; private Object messageHtml; private IContentItem mimeMessage; private String attachmentName; private IContentItem attachmentContent; private String attachmentName2; private IContentItem attachmentContent2; private String attachmentName3; /* * Default constructor */ public SimpleEmailComponent() { } // ---------------------------------------------------------------------------- // BEGIN BEAN METHODS // ---------------------------------------------------------------------------- public String getBcc() { return bcc; } public void setBcc( final String bcc ) { this.bcc = bcc; } public String getCc() { return cc; } public void setCc( final String cc ) { this.cc = cc; } public String getFrom() { return from; } public void setFrom( final String from ) { this.from = from; } public String getSubject() { return subject; } public void setSubject( final String subject ) { this.subject = subject; } public String getTo() { return to; } public void setTo( final String to ) { this.to = to; } public Object getMessageHtml() { return messageHtml; } public void setMessageHtml( final Object messageHtml ) { this.messageHtml = messageHtml; } public Object getMessagePlain() { return messagePlain; } public void setMessagePlain( final Object messagePlain ) { this.messagePlain = messagePlain; } public IContentItem getMimeMessage() { return mimeMessage; } public void setMimeMessage( final IContentItem mimeMessage ) { this.mimeMessage = mimeMessage; } public IContentItem getAttachmentContent() { return attachmentContent; } public void setAttachmentContent( final IContentItem attachmentContent ) { this.attachmentContent = attachmentContent; } public String getAttachmentName() { return attachmentName; } public void setAttachmentName( final String attachmentName ) { this.attachmentName = attachmentName; } public IContentItem getAttachmentContent2() { return attachmentContent2; } public void setAttachmentContent2( final IContentItem attachmentContent2 ) { this.attachmentContent2 = attachmentContent2; } public IContentItem getAttachmentContent3() { return attachmentContent3; } public void setAttachmentContent3( final IContentItem attachmentContent3 ) { this.attachmentContent3 = attachmentContent3; } public String getAttachmentName2() { return attachmentName2; } public void setAttachmentName2( final String attachmentName2 ) { this.attachmentName2 = attachmentName2; } public String getAttachmentName3() { return attachmentName3; } public void setAttachmentName3( final String attachmentName3 ) { this.attachmentName3 = attachmentName3; } private IContentItem attachmentContent3; /** * Sets the mime-type for determining which report output type to generate. This should be a mime-type for consistency * with streaming output mime-types. * * @param outputType * the desired output type (mime-type) for the report engine to generate */ public void setOutputType( final String outputType ) { this.outputType = outputType; } /** * Gets the output type, this should be a mime-type for consistency with streaming output mime-types. * * @return the current output type for the report */ public String getOutputType() { return outputType; } /** * This method sets the map of *all* the inputs which are available to this component. This allows us to use * action-sequence inputs as parameters for our reports. * * @param inputs * a Map containing inputs */ public void setInputs( final Map<String, Object> inputs ) { this.inputs = inputs; if ( inputs.containsKey( INPUT_FROM ) ) { setFrom( (String) inputs.get( INPUT_FROM ) ); } if ( inputs.containsKey( INPUT_TO ) ) { setTo( (String) inputs.get( INPUT_TO ) ); } if ( inputs.containsKey( INPUT_CC ) ) { setCc( (String) inputs.get( INPUT_CC ) ); } if ( inputs.containsKey( INPUT_BCC ) ) { setBcc( (String) inputs.get( INPUT_BCC ) ); } if ( inputs.containsKey( INPUT_SUBJECT ) ) { setSubject( (String) inputs.get( INPUT_SUBJECT ) ); } if ( inputs.containsKey( INPUT_MESSAGEPLAIN ) ) { setMessagePlain( (String) inputs.get( INPUT_MESSAGEPLAIN ) ); } if ( inputs.containsKey( INPUT_MESSAGEHTML ) ) { setMessageHtml( (String) inputs.get( INPUT_MESSAGEHTML ) ); } if ( inputs.containsKey( INPUT_MIMEMESSAGE ) ) { setMimeMessage( (IContentItem) inputs.get( INPUT_MIMEMESSAGE ) ); } } // ---------------------------------------------------------------------------- // END BEAN METHODS // ---------------------------------------------------------------------------- protected Object getInput( final String key, final Object defaultValue ) { if ( inputs != null ) { final Object input = inputs.get( key ); if ( input != null ) { return input; } } return defaultValue; } /** * This method will determine if the component instance 'is valid.' The validate() is called after all of the bean * 'setters' have been called, so we may validate on the actual values, not just the presence of inputs as we were * historically accustomed to. * <p/> * Since we should have a list of all action-sequence inputs, we can determine if we have sufficient inputs to meet * the parameter requirements This would include validation of values and ranges of values. * * @return true if valid * @throws Exception */ public boolean validate() throws Exception { boolean result = true; if ( getTo() == null ) { log.error( Messages.getInstance().getString( "ReportPlugin.emailToNotProvided" ) ); //$NON-NLS-1$ return false; } else if ( getFrom() == null ) { log.error( Messages.getInstance().getString( "ReportPlugin.emailFromNotProvided" ) ); //$NON-NLS-1$ return false; } else if ( ( getMessagePlain() == null ) && ( getMessageHtml() == null ) && ( getMimeMessage() == null ) ) { log.error( Messages.getInstance().getString( "ReportPlugin.emailContentNotProvided" ) ); //$NON-NLS-1$ result = false; } return result; } /** * Perform the primary function of this component, this is, to execute. This method will be invoked immediately * following a successful validate(). * <p/> * This method has 2 ways of working: * <p/> * 1. You supply a mimeMessage: That mimeMessage will be sent; Optionally, contents will be added as attachment and * the original mimeMessage will be encapsulated under a multipart/mixed * <p/> * <p/> * 2. You supply a messageHtml and/or a messageText. A new mimemessage will be built. If you supply both, a * multipart/alternative will be used. After that attachments will be included * * @return true if successful execution * @throws Exception */ public boolean execute() throws Exception { try { // Get the session object final Session session = buildSession(); // Create the message final MimeMessage msg = new MimeMessage( session ); // From, to, etc. applyMessageHeaders( msg ); // Get main message multipart final Multipart multipartBody = getMultipartBody( session ); // Process attachments final Multipart mainMultiPart = processAttachments( multipartBody ); msg.setContent( mainMultiPart ); // Send it msg.setHeader( "X-Mailer", MAILER ); //$NON-NLS-1$ msg.setSentDate( new Date() ); Transport.send( msg ); return true; } catch ( SendFailedException e ) { log.error( Messages.getInstance().getString( "ReportPlugin.emailSendFailed" ) ); //$NON-NLS-1$ } catch ( AuthenticationFailedException e ) { log.error( Messages.getInstance().getString( "ReportPlugin.emailAuthenticationFailed" ) ); //$NON-NLS-1$ } return false; } private Multipart getMultipartBody( final Session session ) throws MessagingException, IOException { // if we have a mimeMessage, use it. Otherwise, build one with what we have // We can have both a messageHtml and messageText. Build according to it MimeMultipart parentMultipart = new MimeMultipart(); MimeBodyPart htmlBodyPart = null, textBodyPart = null; if ( getMimeMessage() != null ) { // Rebuild a MimeMessage and use this one final MimeBodyPart original = new MimeBodyPart(); final MimeMessage originalMimeMessage = new MimeMessage( session, getMimeMessage().getInputStream() ); final MimeMultipart relatedMultipart = (MimeMultipart) originalMimeMessage.getContent(); parentMultipart = relatedMultipart; htmlBodyPart = new MimeBodyPart(); htmlBodyPart.setContent( relatedMultipart ); } // The information we have in the mime-message overrides the getMessageHtml. if ( getMessageHtml() != null && htmlBodyPart != null ) { final String content = getInputString( getMessageHtml() ); htmlBodyPart = new MimeBodyPart(); htmlBodyPart.setContent( content, "text/html; charset=" + LocaleHelper.getSystemEncoding() ); final MimeMultipart htmlMultipart = new MimeMultipart(); htmlMultipart.addBodyPart( htmlBodyPart ); parentMultipart = htmlMultipart; } if ( getMessagePlain() != null ) { final String content = getInputString( getMessagePlain() ); textBodyPart = new MimeBodyPart(); textBodyPart.setContent( content, "text/plain; charset=" + LocaleHelper.getSystemEncoding() ); final MimeMultipart textMultipart = new MimeMultipart(); textMultipart.addBodyPart( textBodyPart ); parentMultipart = textMultipart; } // We have both text and html? Encapsulate it in a multipart/alternative if ( htmlBodyPart != null && textBodyPart != null ) { final MimeMultipart alternative = new MimeMultipart( "alternative" ); alternative.addBodyPart( textBodyPart ); alternative.addBodyPart( htmlBodyPart ); parentMultipart = alternative; } return parentMultipart; } private String getInputString( final Object param ) throws IOException { if ( param instanceof String ) { return (String) param; } else if ( param instanceof IContentItem ) { final InputStream in = ( (IContentItem) param ).getInputStream(); // Convert to String final ByteArrayOutputStream out = new ByteArrayOutputStream(); int nextChar; while ( ( nextChar = in.read() ) != -1 ) { out.write( nextChar ); } return new String( out.toString( LocaleHelper.getSystemEncoding() ) ); } throw new IllegalStateException( "Input is not a String or ContentItem" ); } private Multipart processAttachments( final Multipart multipartBody ) throws MessagingException, IOException { if ( getAttachmentContent() == null ) { // We don't have a first attachment, won't even search for the others. return multipartBody; } // We have attachments; Creating a multipart-mixed final MimeMultipart mixedMultipart = new MimeMultipart( "mixed" ); // Add the first part final MimeBodyPart bodyPart = new MimeBodyPart(); bodyPart.setContent( multipartBody ); mixedMultipart.addBodyPart( bodyPart ); // Process each of the attachments we have processSpecificAttachment( mixedMultipart, getAttachmentContent() ); processSpecificAttachment( mixedMultipart, getAttachmentContent2() ); processSpecificAttachment( mixedMultipart, getAttachmentContent3() ); return mixedMultipart; } private void processSpecificAttachment( final MimeMultipart mixedMultipart, final IContentItem attachmentContent ) throws IOException, MessagingException { // Add this attachment if ( attachmentContent != null ) { final ByteArrayDataSource dataSource = new ByteArrayDataSource( attachmentContent.getInputStream(), attachmentContent.getMimeType() ); final MimeBodyPart attachmentBodyPart = new MimeBodyPart(); attachmentBodyPart.setDataHandler( new DataHandler( dataSource ) ); attachmentBodyPart.setFileName( getAttachmentName() ); mixedMultipart.addBodyPart( attachmentBodyPart ); } } private void applyMessageHeaders( final MimeMessage msg ) throws Exception { msg.setFrom( new InternetAddress( from ) ); msg.setRecipients( Message.RecipientType.TO, InternetAddress.parse( to, false ) ); if ( ( cc != null ) && ( cc.trim().length() > 0 ) ) { msg.setRecipients( Message.RecipientType.CC, InternetAddress.parse( cc, false ) ); } if ( ( bcc != null ) && ( bcc.trim().length() > 0 ) ) { msg.setRecipients( Message.RecipientType.BCC, InternetAddress.parse( bcc, false ) ); } if ( subject != null ) { msg.setSubject( subject, LocaleHelper.getSystemEncoding() ); } } private Session buildSession() throws Exception { final Properties props = new Properties(); try { final Document configDocument = PentahoSystem.getSystemSettings().getSystemSettingsDocument( "smtp-email/email_config.xml" ); //$NON-NLS-1$ final List properties = configDocument.selectNodes( "/email-smtp/properties/*" ); //$NON-NLS-1$ final Iterator propertyIterator = properties.iterator(); while ( propertyIterator.hasNext() ) { final Node propertyNode = (Node) propertyIterator.next(); final String propertyName = propertyNode.getName(); final String propertyValue = propertyNode.getText(); props.put( propertyName, propertyValue ); } } catch ( Exception e ) { log.error( Messages.getInstance().getString( "ReportPlugin.emailConfigFileInvalid" ) ); //$NON-NLS-1$ throw e; } final boolean authenticate = "true".equals( props.getProperty( "mail.smtp.auth" ) ); //$NON-NLS-1$//$NON-NLS-2$ // Get a Session object final Session session; if ( authenticate ) { final Authenticator authenticator = new EmailAuthenticator(); session = Session.getInstance( props, authenticator ); } else { session = Session.getInstance( props ); } // if debugging is not set in the email config file, match the // component debug setting if ( !props.containsKey( "mail.debug" ) ) { //$NON-NLS-1$ session.setDebug( true ); } return session; } /** * This method returns the output-type for the streaming output, it is the same as what is returned by getOutputType() * for consistency. * * @return the mime-type for the streaming output */ public String getMimeType() { return outputType; } private static class EmailAuthenticator extends Authenticator { private EmailAuthenticator() { } @Override protected PasswordAuthentication getPasswordAuthentication() { final String user = PentahoSystem.getSystemSetting( "smtp-email/email_config.xml", "mail.userid", null ); //$NON-NLS-1$ //$NON-NLS-2$ final String password = PentahoSystem.getSystemSetting( "smtp-email/email_config.xml", "mail.password", null ); //$NON-NLS-1$ //$NON-NLS-2$ return new PasswordAuthentication( user, password ); } } }